/* @flow */

import { _Vue } from '../install'
import type Router from '../index'
import { inBrowser } from '../util/dom'
import { runQueue } from '../util/async'
import { warn } from '../util/warn'
import { START, isSameRoute, handleRouteEntered } from '../util/route'
import {
  flatten,
  flatMapComponents,
  resolveAsyncComponents
} from '../util/resolve-components'
import {
  createNavigationDuplicatedError,
  createNavigationCancelledError,
  createNavigationRedirectedError,
  createNavigationAbortedError,
  isError,
  isNavigationFailure,
  NavigationFailureType
} from '../util/errors'

export class History {
  router: Router
  base: string
  current: Route
  pending: ?Route
  cb: (r: Route) => void
  ready: boolean
  readyCbs: Array<Function>
  readyErrorCbs: Array<Function>
  errorCbs: Array<Function>
  listeners: Array<Function>
  cleanupListeners: Function

  // implemented by sub-classes
  +go: (n: number) => void
  +push: (loc: RawLocation, onComplete?: Function, onAbort?: Function) => void
    +replace: (
      loc: RawLocation,
      onComplete?: Function,
      onAbort?: Function
    ) => void
      +ensureURL: (push?: boolean) => void
        +getCurrentLocation: () => string
          + setupListeners: Function

constructor(router: Router, base: ?string) {
  this.router = router
  this.base = normalizeBase(base)
  // start with a route object that stands for "nowhere"
  this.current = START

  this.pending = null
  this.ready = false
  this.readyCbs = []
  this.readyErrorCbs = []
  this.errorCbs = []
  this.listeners = []
}

listen(cb: Function) {
  this.cb = cb
}

onReady(cb: Function, errorCb: ?Function) {
  if (this.ready) {
    cb()
  } else {
    this.readyCbs.push(cb)
    if (errorCb) {
      this.readyErrorCbs.push(errorCb)
    }
  }
}

onError(errorCb: Function) {
  this.errorCbs.push(errorCb)
}

transitionTo(
  location: RawLocation,
  onComplete ?: Function,
  onAbort ?: Function
) {
  let route

  // catch redirect option https://github.com/vuejs/vue-router/issues/3201
  try {
    // match() 传入下个路由path  路由列表中拿出 下个页面的 route 对象   实际上match方法 调用的是 histroy.matcher.match
    // matcher 对象 由createMatcher() 函数返回的    第一个参数传入路由数组routes  第二个参数是 histroy 引用
    // 由createMatcher 函数 以闭包的形式 返回了的对象  这个对象有四个方法
    //                                  return {
    //                                      match,
    //                                      addRoute,
    //                                      getRoutes,
    //                                      addRoutes
    //                                    }
    // baseHistory 的 match 就是调用 这四个方法 中的 match
    // 让我们再看看match 方法
    //  这个方法传入 三个参数  raw （location路由path）   currentRoute(初始的路由对象  类型模板一样的东西)   redirectedFrom
    //  match 方法首先调用了 normalizeLocation 方法  看方法名 标准化 一般化 location 路由  传入了
    //      raw, currentRoute, false, router ，及时用模板 与下一个页面路径 合成成 个路由对象 放回了location
    // 之后又三条线 判断 location  是否存在 name   是否 存在path   两者都没
    // 存在path 时 遍历 pathList 数组。
    // 在扯扯 什么是 pathList 他是干嘛的，有什么用
    // 在 createMatcher 函数一开始调用的时候 内部就调用了  createRouteMap(routes) 方法 传入可routes 数组  返回了一对象 有三个属性
    // pathList, pathMap, nameMap    pathList 是routes 数组里的所有path 组成的数组
    // pathMap  是 routes 数组里的所有path 为属性名 属性 为 RouteRecord 的 map 对象
    // nameMap  是 routes 数组里的所有name 为属性名 属性 为 RouteRecord 的 map 对象
    // 什么是 RouteRecord 对象可以简单理解为 包含每个routes 的信息集合
    // 以 pathMap为例  其名 每个route  path是属性 对应的 组件component，meate 等信息都可以从RouteRecord 获取
    // 说到 遍历 pathList ，拿到每个path 用 pathmap 拿到每个path 对应的 RouteRecord 对象
    //
    //  在内部调用 matchRoute 方法 判断每个 RouteRecord 中 path与 loaction 中path 是一样的 没有就返回false 有就通过 等其他操作
    //  一般就一个， 不会你有两个路由 path 是一样的吧， 那可真是蠢透了。
    //  然后是 调用 _createRoute 方法  内部又 调用 createRoute，返回回了 createRoute 结果：一个对象route，
    // 这个结果就是match 返回的，
    // 综上所述 match()方法  就是根据 传入的 path  ，在 route 列表里 找到 并创建 这个 route对象 并返回。
    route = this.router.match(location, this.current)
  } catch (e) {
    this.errorCbs.forEach(cb => {
      cb(e)
    })
    // Exception should still be thrown
    throw e
  }

  const prev = this.current
  this.confirmTransition(
    route,
    () => {
      this.updateRoute(route)
      onComplete && onComplete(route)
      this.ensureURL()
      this.router.afterHooks.forEach(hook => {
        hook && hook(route, prev)
      })

      // fire ready cbs once
      if (!this.ready) {
        this.ready = true
        this.readyCbs.forEach(cb => {
          cb(route)
        })
      }
    },
    err => {
      if (onAbort) {
        onAbort(err)
      }
      if (err && !this.ready) {
        // Initial redirection should not mark the history as ready yet
        // because it's triggered by the redirection instead
        // https://github.com/vuejs/vue-router/issues/3225
        // https://github.com/vuejs/vue-router/issues/3331
        if (
          !isNavigationFailure(err, NavigationFailureType.redirected) ||
          prev !== START
        ) {
          this.ready = true
          this.readyErrorCbs.forEach(cb => {
            cb(err)
          })
        }
      }
    }
  )
}

confirmTransition(route: Route, onComplete: Function, onAbort ?: Function) {
  const current = this.current
  this.pending = route
  const abort = err => {
    // changed after adding errors with
    // https://github.com/vuejs/vue-router/pull/3047 before that change,
    // redirect and aborted navigation would produce an err == null
    if (!isNavigationFailure(err) && isError(err)) {
      if (this.errorCbs.length) {
        this.errorCbs.forEach(cb => {
          cb(err)
        })
      } else {
        warn(false, 'uncaught error during route navigation:')
        console.error(err)
      }
    }
    onAbort && onAbort(err)
  }
  const lastRouteIndex = route.matched.length - 1
  const lastCurrentIndex = current.matched.length - 1
  console.log('sssssssrote', ...route, ...this.current)

  if (
    isSameRoute(route, current) &&
    // in the case the route map has been dynamically appended to
    lastRouteIndex === lastCurrentIndex &&
    route.matched[lastRouteIndex] === current.matched[lastCurrentIndex]
  ) {
    // console.log('isSameRoute')
    this.ensureURL()
    return abort(createNavigationDuplicatedError(current, route))
  }

  const { updated, deactivated, activated } = resolveQueue(
    this.current.matched,
    route.matched
  )
  const queue: Array<?NavigationGuard> = [].concat(
    // in-component leave guards  // 失活的组件钩子
    extractLeaveGuards(deactivated),
    // global before hooks 全局 beforeEach 钩子
    this.router.beforeHooks,
    // in-component update hooks  在当前路由改变，但是该组件被复用时调用
    extractUpdateHooks(updated),
    // in-config enter guards  需要渲染组件 enter 守卫钩子
    activated.map(m => m.beforeEnter),
    // async components 解析异步路由组件
    resolveAsyncComponents(activated)
  )
  console.log('queue', queue, updated, deactivated, activated)

  const iterator = (hook: NavigationGuard, next) => {
    // console.log('hook', hook, this.pending, route)
    if (this.pending !== route) {
      return abort(createNavigationCancelledError(current, route))
    }
    try {
      hook(route, current, (to: any) => {
        if (to === false) {
          // next(false) -> abort navigation, ensure current URL
          this.ensureURL(true)
          abort(createNavigationAbortedError(current, route))
        } else if (isError(to)) {
          this.ensureURL(true)
          abort(to)
        } else if (
          typeof to === 'string' ||
          (typeof to === 'object' &&
            (typeof to.path === 'string' || typeof to.name === 'string'))
        ) {
          // next('/') or next({ path: '/' }) -> redirect
          abort(createNavigationRedirectedError(current, route))
          if (typeof to === 'object' && to.replace) {
            this.replace(to)
          } else {
            this.push(to)
          }
        } else {
          // confirm transition and pass on the value
          next(to)
        }
      })
    } catch (e) {
      abort(e)
    }
  }

  runQueue(queue, iterator, () => {
    // wait until async components are resolved before
    // extracting in-component enter guards
    const enterGuards = extractEnterGuards(activated)
    console.log('enterGuards', enterGuards)
    const queue = enterGuards.concat(this.router.resolveHooks)
    runQueue(queue, iterator, () => {
      if (this.pending !== route) {
        return abort(createNavigationCancelledError(current, route))
      }
      this.pending = null
      onComplete(route)
      if (this.router.app) {
        this.router.app.$nextTick(() => {
          handleRouteEntered(route)
        })
      }
    })
  })
}

updateRoute(route: Route) {
  this.current = route
  this.cb && this.cb(route)
}

setupListeners() {
  // Default implementation is empty
}

teardown() {
  // clean up event listeners
  // https://github.com/vuejs/vue-router/issues/2341
  this.listeners.forEach(cleanupListener => {
    cleanupListener()
  })
  this.listeners = []

  // reset current history route
  // https://github.com/vuejs/vue-router/issues/3294
  this.current = START
  this.pending = null
}
}

function normalizeBase(base: ?string): string {
  if (!base) {
    if (inBrowser) {
      // respect <base> tag
      const baseEl = document.querySelector('base')
      base = (baseEl && baseEl.getAttribute('href')) || '/'
      // strip full URL origin
      base = base.replace(/^https?:\/\/[^\/]+/, '')
    } else {
      base = '/'
    }
  }
  // make sure there's the starting slash
  if (base.charAt(0) !== '/') {
    base = '/' + base
  }
  // remove trailing slash
  return base.replace(/\/$/, '')
}

function resolveQueue(
  current: Array<RouteRecord>,
  next: Array<RouteRecord>
): {
  updated: Array<RouteRecord>,
  activated: Array<RouteRecord>,
  deactivated: Array<RouteRecord>
} {
  let i
  const max = Math.max(current.length, next.length)
  for (i = 0; i < max; i++) {
    if (current[i] !== next[i]) {
      break
    }
  }
  console.log('current', current.slice(i), next)
  // current = [1,2,3]
  // next = [1,2,3,4,5]
  // i = 4
  // current.slice(i) = []
  return {
    updated: next.slice(0, i),
    activated: next.slice(i),
    deactivated: current.slice(i)
  }
}

function extractGuards(
  records: Array<RouteRecord>,
  name: string,
  bind: Function,
  reverse?: boolean
): Array<?Function> {
  console.log('beforeRouteLeave extractGuards', records)
  const guards = flatMapComponents(records, (def, instance, match, key) => {
    const guard = extractGuard(def, name)
    console.log('def-------', guard, def, instance, match, key) //找出组件中对应的钩子函数
    if (guard) {
      // 给每个钩子函数添加上下文对象为组件自身
      return Array.isArray(guard)
        ? guard.map(guard => bind(guard, instance, match, key))
        : bind(guard, instance, match, key)
    }
  })
  console.log('------------', guards)
  // 数组降维，并且判断是否需要翻转数组
  // 因为某些钩子函数需要从子执行到父
  return flatten(reverse ? guards.reverse() : guards)
}

function extractGuard(
  def: Object | Function,
  key: string
): NavigationGuard | Array<NavigationGuard> {
  console.log('extractGuard', def, key)
  if (typeof def !== 'function') {
    // extend now so that global mixins are applied.
    def = _Vue.extend(def)
    console.log('not function', def, def.options[key])

  }
  return def.options[key]
}

function extractLeaveGuards(deactivated: Array<RouteRecord>): Array<?Function> {
  // 传入需要执行的钩子函数名
  console.log('beforeRouteLeave', deactivated)
  return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)
}

function extractUpdateHooks(updated: Array<RouteRecord>): Array<?Function> {
  return extractGuards(updated, 'beforeRouteUpdate', bindGuard)
}

function bindGuard(guard: NavigationGuard, instance: ?_Vue): ?NavigationGuard {
  console.log('run leave route hook', guard, instance, arguments)
  if (instance) {
    return function boundRouteGuard() {
      return guard.apply(instance, arguments)
    }
  }
}

function extractEnterGuards(activated: Array<RouteRecord>): Array<?Function> {
  return extractGuards(
    activated,
    'beforeRouteEnter',
    (guard, _, match, key) => {
      return bindEnterGuard(guard, match, key)
    }
  )
}

function bindEnterGuard(
  guard: NavigationGuard,
  match: RouteRecord,
  key: string
): NavigationGuard {
  return function routeEnterGuard(to, from, next) {
    return guard(to, from, cb => {
      if (typeof cb === 'function') {
        if (!match.enteredCbs[key]) {
          match.enteredCbs[key] = []
        }
        match.enteredCbs[key].push(cb)
      }
      next(cb)
    })
  }
}
